﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;

using Pg.BioMedics.SDR.Entities;
using Pg.BioMedics.SDR.Data.Constants;

namespace Pg.BioMedics.SDR.Data
{
    /// <summary>
    /// provides entry point for all data access logic routines related with 
    /// Person and its satelite entities.
    /// </summary>
    public class PersonManager : BaseDataManager
    {
        #region Constructor

        /// <summary>
        /// Creates instance of PersonManager. Provides all required initialization
        /// according to selected strategy.
        /// </summary>
        /// <param name="dataMapperStrategy">Data Mapper strategy used by current data context</param>
        internal PersonManager(IDataMapperStrategy dataMapperStrategy)
            : base(dataMapperStrategy) { }

        #endregion

        #region Methods

        /// <summary>
        /// Inserts a new person into the database
        /// </summary>
        /// <param name="person">Not saved person instance to be saved</param>
        /// <returns>Saved person</returns>
        public virtual Person InsertPerson(Person person)
        {
            try
            {
                DataMapper.Insert(PersonStoredProcedures.InsertPerson, person);
                ThrowInsertPersonException(person);

                return person;
            }
            catch (Exception ex)
            {
                throw new SDRDataException("Cannot insert person", ex);
            }
        }

        /// <summary>
        /// Updates existing person in the database
        /// </summary>
        /// <param name="person">Existing person</param>
        /// <returns>Updated person</returns>
        public virtual Person UpdatePerson(Person person)
        {
            try
            {
                DataMapper.Insert(PersonStoredProcedures.UpdatePerson, person);
                ThrowUpdatePersonException(person);

                return person;
            }
            catch (Exception ex)
            {
                throw new SDRDataException("Cannot update person", ex);
            }
        }

        /// <summary>
        /// Sets given person status
        /// </summary>
        /// <param name="person">Existing person</param>
        /// <param name="statusCode">Person status code</param>
        public virtual void SetPersonStatus(Person person, string statusCode)
        {
            try
            {
                Dictionary<string, object> parameters = new Dictionary<string, object>();
                parameters.Add("PersonId", person.Id);
                parameters.Add("StatusCode", statusCode);
                parameters.Add("Result", 0);

                DataMapper.Insert(PersonStoredProcedures.SetPersonStatus, parameters);
                ThrowSetPersonStatusException((int)parameters["Result"], person, statusCode); 
            }
            catch (Exception ex)
            {
                throw new SDRDataException("Cannot set person status", ex);
            }
        }

        /// <summary>
        /// Gets person by id
        /// </summary>
        /// <param name="personId">Person unique id</param>
        /// <returns>Person instance or null if person doesnot exist</returns>
        public virtual Person GetPersonById(int personId)
        {
            try
            {
                Dictionary<string, object> parameters = new Dictionary<string,object>();
                parameters.Add("PersonId", personId);

                return DataMapper.QueryForObject<Person>(PersonStoredProcedures.GetPersonById, parameters);
            }
            catch (Exception ex)
            {
                throw new SDRDataException("Cannot get person by id (id = {0})", ex, personId);
            }
        }

        /// <summary>
        /// Gets person by login
        /// </summary>
        /// <param name="login">Unique system login</param>
        /// <returns>Person if login exists or null if not</returns>
        public virtual Person GetPersonByLogin(string login)
        {
            try
            {
                Dictionary<string, object> parameters = new Dictionary<string, object>();
                parameters.Add("Login", login);

                return DataMapper.QueryForObject<Person>(PersonStoredProcedures.GetPersonByLogin, parameters);
            }
            catch (Exception ex)
            {
                throw new SDRDataException("Cannot get person by login (login = {0})", ex, login);
            }
        }

        /// <summary>
        /// Get List of people registered in the system with given current status
        /// </summary>
        /// <param name="statusCode">Person status code</param>
        /// <returns>List of Person instances</returns>
        public virtual IList<Person> GetPeopleByStatus(string statusCode)
        {
            try
            {
                Dictionary<string, object> parameters = new Dictionary<string,object>();
                parameters.Add("StatusCode", statusCode);
                parameters.Add("Result", 0);

                return DataMapper.QueryForList<Person>(PersonStoredProcedures.GetPeopleByStatus, parameters);
            }
            catch (Exception ex)
            {
                throw new SDRDataException("Cannot get people for status {0}", ex, statusCode);
            }
        }

        /// <summary>
        /// Sets person password hash
        /// </summary>
        /// <param name="person">Person instance</param>
        /// <param name="password">Password to be hashed and stored in database</param>
        public virtual void SetPassword(Person person, string password)
        {
            try
            {
                // calculate SH1 hash
                byte[] buffer = Encoding.Default.GetBytes(password);
                string passwordHash = Convert.ToBase64String(SHA1.Create().ComputeHash(buffer));

                // execute stored procedure
                Dictionary<string, object> parameters = new Dictionary<string, object>();
                parameters.Add("PersonId", person.Id);
                parameters.Add("PasswordHash", passwordHash);

                DataMapper.Update(PersonStoredProcedures.SetPasswordHash, parameters);
                ThrowSetPasswordHashException((int)parameters["Result"], person);
            }
            catch (Exception ex)
            {
                throw new SDRDataException("Cannot set password for person {0}", ex, person.Id);
            }
        }

        /// <summary>
        /// Checks if user with given login exists in the database. Additionaly 
        /// if user exists checks if password is valid
        /// </summary>
        /// <param name="login">Person login</param>
        /// <param name="password">Person password</param>
        /// <returns>Returns true if user login and password are valid</returns>
        public virtual bool ValidateCredentials(string login, string password)
        {
            try
            {
                // calculate SH1 hash
                byte[] buffer = Encoding.Default.GetBytes(password);
                string passwordHash = Convert.ToBase64String(SHA1.Create().ComputeHash(buffer));

                // execute stored procedure
                Dictionary<string, object> parameters = new Dictionary<string, object>();
                parameters.Add("Login", login);
                parameters.Add("PasswordHash", passwordHash);

                DataMapper.Insert(PersonStoredProcedures.ValidateCredentials, parameters);                
                return ((int)parameters["Result"] == 0);
            }
            catch (Exception ex)
            {
                throw new SDRDataException("Cannot validate person credentials for login {0}", ex, login);
            }
        }

        /// <summary>
        /// Inserts a new PlayedRole in the system
        /// </summary>
        /// <param name="playedRole">Not saved PlayedRole</param>
        /// <returns>Saved PlayedRole</returns>
        public virtual PlayedRole InsertPlayedRole(PlayedRole playedRole)
        {
            try
            {
                DataMapper.Insert(PersonStoredProcedures.InsertPlayedRole, playedRole);
                ThrowInsertPlayedRoleException(playedRole);

                return playedRole;
            }
            catch (Exception ex)
            {
                throw new SDRDataException("Cannot insert played role", ex);
            }
        }

        /// <summary>
        /// Sets playead role status
        /// </summary>
        /// <param name="playedRole">Existing played role</param>
        /// <param name="statusCode">Played role status</param>
        public virtual void SetPlayedRoleStatus(PlayedRole playedRole, string statusCode)
        {
            try
            {
                Dictionary<string, object> parameters = new Dictionary<string,object>();
                parameters.Add("PlayedRoleId", playedRole.Id);
                parameters.Add("StatusCode", statusCode);

                DataMapper.Insert(PersonStoredProcedures.SetPlayedRoleStatus, parameters);
                ThrowSetPlayedRoleStatusException((int)parameters["Result"], playedRole, statusCode);
            }
            catch (Exception ex)
            {
                throw new SDRDataException("Cannot set played role status", ex);
            }
        }

        /// <summary>
        /// Get list of roles played by the given person
        /// </summary>
        /// <param name="person">Registered person</param>
        /// <param name="statusCode">Played role status code</param>
        /// <returns>List of played roles</returns>
        public virtual IList<PlayedRole> GetPlayedRoles(Person person, string statusCode)
        {
            try
            {
                Dictionary<string, object> parameters = new Dictionary<string,object>();
                parameters.Add("PersonId", person.Id);
                parameters.Add("StatusCode", statusCode);

                return DataMapper.QueryForList<PlayedRole>(PersonStoredProcedures.GetPlayedRoles, parameters);
            }
            catch (Exception ex)
            {
                throw new SDRDataException("Cannot get roles played by person {0} in status {1}", ex, person.Id, statusCode);
            }
        }

        /// <summary>
        /// Gets all people who have given role assigned with given status. 
        /// </summary>
        /// <param name="roleCode">Role code</param>
        /// <param name="roleStatusCode">Role status code</param>
        /// <returns>List of people</returns>
        public virtual IList<Person> GetPeopleInRole(string roleCode, string roleStatusCode)
        {
            try
            {
                Dictionary<string, object> parameters = new Dictionary<string, object>();
                parameters.Add("RoleCode", roleCode);
                parameters.Add("RoleStatusCode", roleStatusCode);

                return DataMapper.QueryForList<Person>(PersonStoredProcedures.GetPeopleInRole, parameters);
            }
            catch (Exception ex)
            {
                throw new SDRDataException("Cannot get list of pleople in role {0}", ex, roleCode);
            }
        }

        #endregion

        #region Support methods

        /// <summary>
        /// Throws appropriate exception if InsertPerson stored procedure returned 
        /// error code instead of positive id.
        /// </summary>
        /// <param name="person">Person entity</param>
        private void ThrowInsertPersonException(Person person)
        {
            switch (person.Id)
            {
                case -1:
                    throw new SDRDataException("Login {0} already exists", person.Login);
                case -2:
                    throw new SDRDataException("Email {0} already exists", person.Email);
            }
        }

        /// <summary>
        /// Throws appropriate exception if UpdatePerson stored procedure returned 
        /// error code instead of positive id
        /// </summary>
        /// <param name="person"></param>
        private void ThrowUpdatePersonException(Person person)
        {
            switch (person.Id)
            {
                case -1:
                    throw new SDRDataException("Person {0} does not exist", person.Id);
                case -2:
                    throw new SDRDataException("Login {0} already exists", person.Login);
                case -3:
                    throw new SDRDataException("Email {0} already exists", person.Email);
            }
        }

        /// <summary>
        /// Throws appropriate exception if SetPersonStatus procedure returns 
        /// erro code
        /// </summary>
        /// <param name="result">Return code</param>
        /// <param name="person"></param>
        /// <param name="statusCode"></param>
        private void ThrowSetPersonStatusException(int result, Person person, string statusCode)
        {
            switch (result)
            {
                case -1:
                    throw new SDRDataException("Person {0} doesnot exist", person.Id);
                case -2:
                    throw new SDRDataException("Status code {0} is invalid", statusCode);
            }
        }

        /// <summary>
        /// Throws appropriate exception if SetPasswordHash stored procedure
        /// returnes error code
        /// </summary>
        /// <param name="result">Stored procedure error code</param>
        /// <param name="person">Person instance</param>
        private void ThrowSetPasswordHashException(int result, Person person)
        {
            switch (result)
            {
                case -1:
                    throw new SDRDataException("Person {0} doesnnot exist", person.Id);
            }
        }

        /// <summary>
        /// Throws appropriate exception if InsertPlayedRole stored procedure 
        /// returns error code
        /// </summary>
        /// <param name="playedRole"></param>
        private void ThrowInsertPlayedRoleException(PlayedRole playedRole)
        {
            switch (playedRole.Id)
            {
                case -1:
                    throw new SDRDataException("Role code {0} is invalid", playedRole.RoleCode);
                case -2:
                    throw new SDRDataException("Person {0} doesnot exist", playedRole.PersonId);
            }
        }

        /// <summary>
        /// Throws appropriate exception if SetPlayedRolestatus stored procedure returns 
        /// erro code
        /// </summary>
        /// <param name="result">The stored procedure error code</param>
        /// <param name="playedRole"></param>
        /// <param name="statusCode"></param>
        private void ThrowSetPlayedRoleStatusException(int result, PlayedRole playedRole, string statusCode)
        {
            switch (result)
            {
                case -1:
                    throw new SDRDataException("Played role {0} doesnot exist", playedRole.Id);
                case -2:
                    throw new SDRDataException("Played role status code {0} doesnot exist", statusCode);
            }
        }

        #endregion
    }
}
